﻿/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A
 * copy of the license can be found at http://go.microsoft.com/fwlink/?LinkID=131993. If
 * you cannot locate the Microsoft Public License, please send an email to
 * mixon@microsoft.com. By using this source code in any fashion, you are agreeing to
 * be bound by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections.Generic;
using System.Net;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Xml.Linq;

namespace MIXOnline.Descry
{
    public partial class AttachableMultiScaleImage : UserControl
    {
        public AttachableMultiScaleImage()
        {
            InitializeComponent();

            SubscribeToEvents();
        }

        #region Events

        public event EventHandler<EventArgs> HelpClicked;

        internal virtual void OnHelpClicked(EventArgs e)
        {
            var eventHandler = this.HelpClicked;
            if (eventHandler != null)
            {
                eventHandler(this, e);
            }
        }

        /// <summary>
        /// If the control is resized, clip off the parts of the pinned objects residing
        /// outside of the control bounds and reset AWND to original position. Lastly move
        /// the markers to their new position.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AttachableMultiScaleImage_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            // clip off anything that is outside of the bounds of this rectangle
            _scaleFactor = canvasPinnedObjects.ActualWidth;
            RectangleGeometry r = new RectangleGeometry();
            r.Rect = new Rect(0, 0, _scaleFactor, canvasPinnedObjects.ActualHeight);
            canvasPinnedObjects.Clip = r;

            // this cannot fire at startup because it will screw up the initial animation.
            if (ranInitialTrackAnimationMethod)
            {
                GoHome();
                TrackAnimation();
            }
        }


        /// <summary>
        /// This event determines which of the 3 types of Markers to display
        /// and positions them accordingly as hover dialogs
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pin_MouseEnter(object sender, MouseEventArgs e)
        {
            // free floating dialogs must all be hidden

            popMarker.Visibility = Visibility.Collapsed; // hide it
            characterMarker.Visibility = Visibility.Collapsed; // hide it
            if (!bShowProcessMarker)
            {
                processMarker.Visibility = Visibility.Collapsed; // hide it if its not just a hover dialog
            }

            Pin pin = sender as Pin;

            Point MarkerCoords;

            Point PinCoords = GetPinScreenCoordinates(pin);

            switch ((pin.DataContext as DataPoint).Type)
            {
                case "PopMarker":
                    popMarker.DataContext = pin.DataContext; //bind
                    popMarker.Visibility = Visibility.Visible; // show it

                    popMarker.SizeChanged += delegate(object sender2, SizeChangedEventArgs e2)
                    {
                        // Position marker in proper quadrant based on Pin coordinates
                        MarkerCoords = MarkerPosition(pin, PinCoords, new Size(popMarker.ActualWidth, popMarker.ActualHeight));
                        Canvas.SetTop(popMarker, MarkerCoords.Y);
                        Canvas.SetLeft(popMarker, MarkerCoords.X);
                    };
                    break;

                case "CharacterMarker":
                    characterMarker.DataContext = pin.DataContext;
                    characterMarker.Visibility = Visibility.Visible; // show it

                    characterMarker.SizeChanged += delegate(object sender2, SizeChangedEventArgs e2)
                    {
                        // Position marker in proper quadrant based on Pin coordinates
                        MarkerCoords = MarkerPosition(pin, PinCoords, new Size(characterMarker.ActualWidth, characterMarker.ActualHeight));
                        Canvas.SetTop(characterMarker, MarkerCoords.Y);
                        Canvas.SetLeft(characterMarker, MarkerCoords.X);
                    };
                    break;

                case "ProcessMarker":

                    // ProcessMarker is just turned on as a hover dialog in the MouseEnter event

                    _processMarkerPositioningPin = pin;
                    processMarker.DataContext = pin.DataContext;
                    //bShowProcessMarker = true; // do not uncomment this line. It was left it here to make sure that it is not accidentally recreated.

                    // only run this code if the marker is not already showing as Dialog
                    if (!bShowProcessMarker)
                    {
                        processMarker.Visibility = Visibility.Visible; // show it
                        MarkerCoords = MarkerPosition(pin, PinCoords, new Size(650, 230)); //don't need delegate because width is static
                        Canvas.SetTop(processMarker, MarkerCoords.Y);
                        Canvas.SetLeft(processMarker, MarkerCoords.X);
                    }

                    break;
            }
        }

        /// <summary>
        /// Hide Dialogs when mouse is not over a Pin
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pin_MouseLeave(object sender, MouseEventArgs e)
        {
            Pin pin = sender as Pin;

            switch ((pin.DataContext as DataPoint).Type)
            {
                case "PopMarker":
                    popMarker.Visibility = Visibility.Collapsed; // hide it
                    break;

                case "CharacterMarker":
                    characterMarker.Visibility = Visibility.Collapsed; // hide it
                    break;

                case "ProcessMarker":
                    // only run this code if the marker is not already showing as Dialog
                    if (!bShowProcessMarker)
                    {
                        processMarker.Visibility = Visibility.Collapsed; // hide it
                    }
                    break;
            }
        }

        /// <summary>
        /// Fires when any Marker is clicked
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Pin pin = sender as Pin;

            // hide any hover or modal dialogs that might be open
            processMarker.Visibility = Visibility.Collapsed;
            bShowProcessMarker = false;
            characterMarker.Visibility = Visibility.Collapsed;
            popMarker.Visibility = Visibility.Collapsed;

            switch ((pin.DataContext as DataPoint).Type)
            {
                case "ProcessMarker": // show the ProcessMarker as a modal dialog
                    
                    processMarker.DataContext = pin.DataContext; // assign selected pin so we can bind data

                    // .5 means 50% of control width and .3 mean 30% of the control height from its top.
                    ZoomImage.ViewportOrigin = GetScreenLogicalCoordinates(pin, .5, .3); 

                    Zoom(.4, pin.Location);

                    //trigger the pin animation to the new zoom settings
                    TrackAnimation();

                    // show the dialog
                    _processMarkerPositioningPin = pin;
                    processMarker.Visibility = Visibility.Visible;
                    bShowProcessMarker = true;

                    break;
            }
        }

        /// <summary>
        /// Fires when the file containing the DataPoint data has finished loading or has failed to load
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void File_Loaded(object sender, DownloadStringCompletedEventArgs e)
        {
            /* 
             * Parse file and programmatically add a "Pin" object for every DataPoint
             * in the XML file. Once done begin the intro animation
             */

            if (e.Error == null)
            {
                string xmlData = e.Result;
                XElement root = XElement.Load(XmlHelper.GetXmlReaderFromString(xmlData));

                IEnumerable<XElement> points = root.Elements("Point");

                foreach (XElement point in points)
                {
                    DataPoint dp = CreateDataPoint(point); // create a DataPoint object for every entry in XML

                    if (dp != null)
                    {
                        dataPoints.Add(dp); // add to the list
                        AddPinToCanvas(canvasPinnedObjects, dp); // position object on canvas
                    }
                }

                GoHome(); // center DeepZoom at initial position

                this.Source = "../awnd_dz_images/dzc_output.xml"; // set the source *after* doing the ZoomImage resize, otherwise animations will display in new zoom.

                // Prepare the Pins for the initial animation by moving them off screen (up 1500px)
                foreach (UIElement uie in canvasPinnedObjects.Children)
                {
                    Pin pin = uie as Pin;
                    Point l = GetPinScreenCoordinates(pin);
                    Canvas.SetTop(pin, l.Y - 1500); // set up for animation.
                    Canvas.SetLeft(pin, l.X);
                }

                // Animate Pins to final resting position in a rain-like way.
                foreach (UIElement uie in canvasPinnedObjects.Children)
                {
                    double restingpoint = Canvas.GetTop(uie as Pin) + 1500;

                    Storyboard storyBoard = new Storyboard();
                    Duration duration = new Duration(TimeSpan.FromMilliseconds(1500));
                    storyBoard.Duration = duration;

                    storyBoard.BeginTime = TimeSpan.FromMilliseconds(Convert.ToDouble(r.Next(0, 1500)));

                    DoubleAnimation animation = new DoubleAnimation();
                    animation.Duration = duration;
                    animation.Completed += new EventHandler(initialAnimation_Completed);
                    animation.FillBehavior = FillBehavior.Stop;
                    storyBoard.Children.Add(animation);

                    Storyboard.SetTarget(animation, uie);
                    Storyboard.SetTargetProperty(animation, new PropertyPath("(Canvas.Top)"));
                    animation.To = restingpoint;

                    storyboards.Add(storyBoard);
                }

                // Start the animations
                foreach (Storyboard storyboard in storyboards)
                {
                    storyboard.Begin();
                }

                if (bStartTour)
                    StartTour();

            }
        }

        /// <summary>
        /// Check to see if any of the buttons on the ProcessMarker are clicked and 
        /// act accordingly
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessMarker_ButtonClicked(object sender, MarkerEventArgs e)
        {
            MarkerEvent b = e.Data;

            int index = -1;

            DataPoint processMarkerData = (processMarker.DataContext as DataPoint);

            if (b == MarkerEvent.Next)
            {
                // get the next process DataPoint pin
                index = processMarkerData.Index.Value;
                index++;
                if (index > 5) index = 1; // round robin
            }
            else if (b == MarkerEvent.Previous)
            {
                // get previous process DataPoint pin
                index = processMarkerData.Index.Value;
                index--;
                if (index < 1) index = 5; // round robin
            }

            if (b == MarkerEvent.Close)
            {
                bShowProcessMarker = false;
            }
            else
            {
                if (index > 0)
                {
                    Pin pin = FindPinByIndex(index);
                    if (pin != null)
                    {
                        // always do ViewPortOrigin before zoom for consistency
                        ZoomImage.ViewportOrigin = GetScreenLogicalCoordinates(pin, .5, .3);
                        Zoom(.4, pin.Location);

                        processMarker.DataContext = pin.DataContext;
                        _processMarkerPositioningPin = pin; // update the pin that the processMarker is using as a Location Reference

                        bShowProcessMarker = true;

                        TrackAnimation();
                    }
                }
            }
        }

        /// <summary>
        /// Animate the Markers to their intial positions 
        /// this can only be done after the initial animation is complete
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void initialAnimation_Completed(object sender, EventArgs e)
        {
            if (!ranInitialTrackAnimationMethod)
            {
                ranInitialTrackAnimationMethod = true;
                TrackAnimation();
            }
        }

        /// <summary>
        /// When a PinSync Animation completes we call it again recursively until 4 seconds have passed.
        /// This allows for smooth animation of the Pins as the DeepZoom image is resized.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pinSyncAnimation_Completed(object sender, EventArgs e)
        {
            double progress = (DateTime.Now - this.startTime).TotalSeconds / animationDuration.TotalSeconds;

            if (progress >= 4)
            {
                _pinSyncAnimation.Completed -= new EventHandler(pinSyncAnimation_Completed); // remove handler so GC can destroy object
                _pinSyncAnimation = null; // destroy animation if time is greater than 4 seconds
            }
            else
            {
                // do what you need to do to synchronize the animation here
                foreach (FrameworkElement fe in canvasPinnedObjects.Children)
                {
                    if (fe is Pin)
                    {
                        SyncPin(fe as Pin);
                    }
                }

                _pinSyncAnimation.Begin();
            }

            // Display and Animate ProcessMarker, if it needs to be displayed
            if (bShowProcessMarker)
                PositionProcessMarker();
        }

        private void ZoomImage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // hide any processMarker that might be showing
            processMarker.Visibility = Visibility.Collapsed;
            bShowProcessMarker = false;

            // note last mouse position and DeepZoom ViewportOrigin
            lastMouseDownPos = e.GetPosition(this.ZoomImage);
            lastMouseViewPort = this.ZoomImage.ViewportOrigin;
            _isDragging = true;

            TrackAnimation();
        }

        private void ZoomImage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            _isDragging = false;
        }

        private void ZoomImage_MouseMove(object sender, MouseEventArgs e)
        {
            // move DeepZoom Image around
            lastMousePos = e.GetPosition(this.ZoomImage);
            if (_isDragging)
            {
                Point newPoint = lastMouseViewPort;
                newPoint.X += (lastMouseDownPos.X - lastMousePos.X) / this.ZoomImage.ActualWidth * this.ZoomImage.ViewportWidth;
                newPoint.Y += (lastMouseDownPos.Y - lastMousePos.Y) / this.ZoomImage.ActualWidth * this.ZoomImage.ViewportWidth;
                this.ZoomImage.ViewportOrigin = newPoint;
                TrackAnimation();
            }
        }

        private void ZoomImage_ImageOpenSucceeded(object sender, RoutedEventArgs e)
        {
        }

        #region UI buttons

        private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            canvasOpacity.Opacity = sliderOpacity.Value;
        }

        private void toggleButtonShowMarkers_Click(object sender, RoutedEventArgs e)
        {
            if (toggleButtonShowMarkers.IsChecked.Value)
            {
                canvasPinnedObjects.Visibility = Visibility.Collapsed;
                toggleButtonShowMarkers.Content = "Show Markers";
            }
            else
            {
                canvasPinnedObjects.Visibility = Visibility.Visible;
                toggleButtonShowMarkers.Content = "Hide Markers";
            }
        }

        private void imageBanner_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            HtmlPage.Window.Navigate(new Uri("http://www.twitter.com/sitenameddesire"), "_blank");
        }

        private void imageBanner_MouseEnter(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_sash_over.png", UriKind.Relative));
        }

        private void imageBanner_MouseLeave(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_sash_up.png", UriKind.Relative));
        }

        private void imageZoomIn_MouseEnter(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_+_over.png", UriKind.Relative));
        }

        private void imageZoomIn_MouseLeave(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_+_up.png", UriKind.Relative));
        }

        private void imageZoomOut_MouseEnter(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_-_over.png", UriKind.Relative));
        }

        private void imageZoomOut_MouseLeave(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_-_up.png", UriKind.Relative));
        }

        private void imageHelp_MouseEnter(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_help_over.png", UriKind.Relative));
        }

        private void imageHelp_MouseLeave(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_help_up.png", UriKind.Relative));
        }

        private void imageZoomIn_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_+_down.png", UriKind.Relative));
            Zoom(_zoom / 1.3, this.ZoomImage.ElementToLogicalPoint(new Point(this.ActualWidth / 2, this.ActualHeight / 2)));
            TrackAnimation();
        }

        private void imageZoomOut_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_-_down.png", UriKind.Relative));
            Zoom(_zoom * 1.3, this.ZoomImage.ElementToLogicalPoint(new Point(this.ActualWidth / 2, this.ActualHeight / 2)));
            TrackAnimation();
        }

        private void imageHome_MouseEnter(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_home_over.png", UriKind.Relative));
        }

        private void imageHome_MouseLeave(object sender, MouseEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_home_up.png", UriKind.Relative));
        }

        private void imageHome_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_home_down.png", UriKind.Relative));
            GoHome();
            TrackAnimation();
        }

        private void imageHelp_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            bShowProcessMarker = false;
            processMarker.Visibility = Visibility.Collapsed;
            // launch UX page in Page.xaml
            (sender as Image).Source = new BitmapImage(new Uri("../images/menu_help_down.png", UriKind.Relative));
            this.OnHelpClicked(new EventArgs());
        }

        #endregion UI buttons

        #endregion Events

        #region Public Methods

        /// <summary>
        /// Load Data and trigger animation when it is done loading
        /// </summary>
        /// <param name="StartTour"></param>
        public void BeginLoadingSource(bool StartTour)
        {
            bStartTour = StartTour;
            LoadFile("AWND_DataPoints.xml");
        }

        /// <summary>
        /// Starts the ProcessMarker for Step A, for first-run UX
        /// </summary>
        public void StartTour()
        {
            Pin pin = FindPinByName("Step A");
            processMarker.DataContext = pin.DataContext; // assign selected pin so we can bind data
            ZoomImage.ViewportOrigin = GetScreenLogicalCoordinates(pin, .5, .3);

            Zoom(.4, pin.Location);

            TrackAnimation();

            _processMarkerPositioningPin = pin;
            processMarker.Visibility = Visibility.Visible;

            // Position marker based on Pin coordinates after zoom
            bShowProcessMarker = true;
        }

        #endregion Public Methods

        #region Methods

        /// <summary>
        /// Dialog position logic which displays dialogs bases on the quadrant a point is in a rectangle.
        /// It uses a 3 column 2 row rectangle to determine proper placement of the dialog inside the control area.
        /// We could've used rectangle intersection to determine this as well.
        /// </summary>
        /// <param name="pin"></param>
        /// <param name="PinCoords"></param>
        /// <param name="width"></param>
        /// <returns></returns>
        private Point MarkerPosition(Pin pin, Point PinCoords, Size DialogSize)
        {
            Point offset = new Point();

            DataPoint dp = (pin.DataContext as DataPoint);

            Size imageSize = new Size(dp.Width.Value * _imageScaleFactor, dp.Height.Value * _imageScaleFactor);

            if (PinCoords.X > this.ActualWidth / 3 * 2)
            {
                // display on left aide of Pin
                offset.X = -DialogSize.Width - imageSize.Width - 10;
            }
            else if (PinCoords.X > this.ActualWidth / 3)
            {
                // center about pin
                offset.X = -DialogSize.Width / 2 - imageSize.Width / 2;
            }
            else
            {
                // display on right of Pin
                offset.X = imageSize.Width + 10;
            }

            if (PinCoords.Y > this.ActualHeight / 2)
            {
                // display above Pin
                offset.Y = -DialogSize.Height - 10;
            }
            else
            {
                // display below Pin
                offset.Y = imageSize.Height + 10;
            }

            return new Point(PinCoords.X + offset.X, PinCoords.Y + offset.Y);
        }

        /// <summary>
        /// Important function to assist in positioning ZoomImage to a specific location
        /// /// </summary>
        /// <param name="pin"></param>
        /// <param name="PercentX"></param>
        /// <param name="percentY"></param>
        /// <returns></returns>
        private Point GetScreenLogicalCoordinates(Pin pin, double PercentX, double percentY)
        {
            double PercentageViewportHeight = (ZoomImage.ViewportWidth * this.ActualHeight / this.ActualWidth) * percentY;
            double PercentageViewportWidth = ZoomImage.ViewportWidth * PercentX;
            return new Point(pin.Location.X - PercentageViewportWidth, pin.Location.Y - PercentageViewportHeight);
        }

        /// <summary>
        /// Position the Marker based on last _processMarkerPositioningPin location
        /// </summary>
        private void PositionProcessMarker()
        {
            DataPoint dp = (_processMarkerPositioningPin.DataContext as DataPoint);
            Size size = new Size(dp.Width.Value * _imageScaleFactor, dp.Height.Value * _imageScaleFactor);

            Point PinCoords = GetPinScreenCoordinates(_processMarkerPositioningPin);
            Point ProcessMarkerPosition = new Point(PinCoords.X - 300, PinCoords.Y + size.Height + 10);

            Canvas.SetLeft(processMarker, ProcessMarkerPosition.X);
            Canvas.SetTop(processMarker, ProcessMarkerPosition.Y);
            processMarker.Visibility = Visibility.Visible;
        }

        /// <summary>
        /// Load the file that contains the "A Website Named Desire" Points of Interest
        /// </summary>
        /// <param name="filename">A relative path to a file.</param>
        private void LoadFile(string filename)
        {
            WebClient client = new WebClient();
            client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(File_Loaded);
            client.DownloadStringAsync(new Uri(filename, UriKind.Relative));
        }

        /// <summary>
        /// Creates a "DataPoint" object from and XML node
        /// Attempts to do some error checking and creates
        /// default values in some cases.
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private DataPoint CreateDataPoint(XElement node)
        {
            Point LogicalLocation = new Point();

            XElement ReferenceWidth = node.Element("ReferenceWidth");
            XElement X = node.Element("X");
            XElement Y = node.Element("Y");
            XElement Name = node.Element("Name");
            XElement Index = node.Element("Index");
            XElement Text = node.Element("Text");
            XElement ImagePath = node.Element("ImagePath");
            XElement ImagePathMouseOver = node.Element("ImagePathMouseOver");
            XElement Width = node.Element("Width");
            XElement Height = node.Element("Height");
            XElement ScaleCenterX = node.Element("ScaleCenterX");
            XElement ScaleCenterY = node.Element("ScaleCenterY");
            XElement Type = node.Element("Type");
            XElement RefCharacterMarkerName = node.Element("RefCharacterMarkerName");

            if (ReferenceWidth != null && X != null && Y != null && !string.IsNullOrEmpty(X.Value) && !string.IsNullOrEmpty(Y.Value))
            {
                double referenceWidth = double.Parse(ReferenceWidth.Value);
                double x = double.Parse(X.Value);
                double y = double.Parse(Y.Value);
                LogicalLocation = new Point(x / referenceWidth, y / referenceWidth);
            }
            else throw new Exception("no location data found");

            if (Name == null)
                throw new Exception("'Name' cannot be null");

            if (Index == null)
            {
                Index = new XElement("Index");
                Index.Value = "0";
            }

            // create a new object for each item in collection
            DataPoint dp = new DataPoint()
            {
                Location = LogicalLocation,
                Name = Name.Value,
                Index = int.Parse(Index.Value),
            };

            if (Text != null)
                dp.Text = Text.Value;

            if (ImagePath != null)
                dp.ImagePath = ImagePath.Value;

            if (ImagePathMouseOver != null)
                dp.ImagePathMouseOver = ImagePathMouseOver.Value;

            if (Width != null && !string.IsNullOrEmpty(Width.Value))
                dp.Width = double.Parse(Width.Value);

            if (Height != null && !string.IsNullOrEmpty(Height.Value))
                dp.Height = double.Parse(Height.Value);

            //if (RefCharacterMarkerName != null && !string.IsNullOrEmpty(RefCharacterMarkerName.Value))
            //    dp.RefCharacterMarkerName = RefCharacterMarkerName.Value;

            if (ScaleCenterX != null && ScaleCenterY != null && !string.IsNullOrEmpty(ScaleCenterX.Value) && !string.IsNullOrEmpty(ScaleCenterY.Value))
            {
                double imageWidthFactor = double.Parse(Width.Value);
                double imageHeightFactor = double.Parse(Height.Value);
                double xScale = double.Parse(ScaleCenterX.Value) / imageWidthFactor;
                double yScale = double.Parse(ScaleCenterY.Value) / imageHeightFactor;
                dp.ScaleCenter = new Point(xScale, yScale);
            }

            if (Type != null && !string.IsNullOrEmpty(Type.Value))
            {
                dp.Type = Type.Value;
            }

            return dp;
        }

        /// <summary>
        /// Draw a pin with "powerlaw scaling" relative to the position of the map multi scale image.
        /// Note that the Pin itself or its contents are not being scaled as might be thought by the name
        /// </summary>
        /// <param name="pinToDraw"></param>
        private void SyncPin(Pin pinToDraw)
        {
            Point where = pinToDraw.Location;
            Nullable<Point> ScaleCenter = pinToDraw.ScaleCenter;

            // figure out the origin of the multiscale image in normalized [0..1] coordinate space
            Rect msiRect = GetImageRect(this.ZoomImage);

            // need the ZoomFactor to figure out where to layout the pin relative to the normalized origin of the MSI
            double zoomFactor = Math.Log(1 / this.ZoomImage.ViewportWidth, 2);

            //System.Diagnostics.Debug.WriteLine(zoomFactor.ToString());

            // take into account zoom factor when figuring out the layout
            Point pinOrigin = new Point(msiRect.X + Math.Pow(2, zoomFactor) * where.X, msiRect.Y + Math.Pow(2, zoomFactor) * where.Y);

            // use "powerlaw scaling" to position the pin
            DrawPin(pinToDraw, pinOrigin, Math.Pow(0.02 * (zoomFactor + 1), 2) + 0.01, ScaleCenter);


            //// Scale Pin using same ZoomFactor
            //if ((zoomFactor + 1.2) < 1) 
            //    _imageScaleFactor = 1;
            //else
            //    _imageScaleFactor = zoomFactor + 1.5;


            // Scale Pin using same ZoomFactor
            if ((zoomFactor + 1.5) < 1)
                _imageScaleFactor = 1;
            else
                _imageScaleFactor = zoomFactor + 1.5;


            pinToDraw.image.Width = (pinToDraw.DataContext as DataPoint).Width.Value * _imageScaleFactor;
            pinToDraw.image.Height = (pinToDraw.DataContext as DataPoint).Height.Value * _imageScaleFactor;
        }

        /// <summary>
        /// Iterate through list of Pins and synchronize position with Zoomable Image
        /// </summary>
        private void SyncAllPins()
        {
            foreach (FrameworkElement fe in canvasPinnedObjects.Children)
            {
                if (fe is Pin)
                {
                    SyncPin(fe as Pin);
                }
            }
        }

        /// <summary>
        /// Draw the pin in normalized [0..1] space to match the MSI behavior
        /// </summary>
        /// <param name="element"></param>
        /// <param name="p"></param>
        /// <param name="w"></param>
        /// <param name="ScaleCenter"></param>
        private void DrawPin(FrameworkElement element, Point p, double w, Nullable<Point> ScaleCenter)
        {
            if (ScaleCenter != new Point())
            {
                // ScaleCenter takes into account the fact that the tip of the pin
                // doesn't align exactly with the edges of the image
                Canvas.SetLeft(element, (p.X - ScaleCenter.Value.X * w) * _scaleFactor);
                Canvas.SetTop(element, (p.Y - ScaleCenter.Value.Y * w) * _scaleFactor);
            }
            else
            {
                Canvas.SetLeft(element, (p.X * _scaleFactor));
                Canvas.SetTop(element, (p.Y * _scaleFactor));
            }
        }

        /// <summary>
        /// Determines the absolute postion of a Pin based on the users current 
        /// position and zoom factor of the MultiScaleImage.
        /// </summary>
        /// <param name="pinToDraw"></param>
        /// <returns></returns>
        private Point GetPinScreenCoordinates(Pin pinToDraw)
        {
            Point where = pinToDraw.Location;
            Nullable<Point> ScaleCenter = pinToDraw.ScaleCenter;

            // Figure out the origin of the MultiScaleImage in normalized [0..1] coordinate space
            Rect msiRect = GetImageRect(this.ZoomImage);

            // need the ZoomFactor to figure out where to layout the pin relative to the normalized origin of the MSI
            double zoomFactor = Math.Log(1 / this.ZoomImage.ViewportWidth, 2);

            // take into account zoom factor when figuring out the layout
            Point pinOrigin = new Point(msiRect.X + Math.Pow(2, zoomFactor) * where.X, msiRect.Y + Math.Pow(2, zoomFactor) * where.Y);

            // use powerlaw scaling for the size of the pin
            return GetPinScreenCoordinatesBasedOnScaleCenter(pinToDraw, pinOrigin, Math.Pow(0.02 * (zoomFactor + 1), 2) + 0.01, ScaleCenter);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="element"></param>
        /// <param name="p"></param>
        /// <param name="w"></param>
        /// <param name="ScaleCenter"></param>
        /// <returns></returns>
        private Point GetPinScreenCoordinatesBasedOnScaleCenter(FrameworkElement element, Point p, double w, Nullable<Point> ScaleCenter)
        {
            if (ScaleCenter != new Point())
            {
                return new Point((p.X - ScaleCenter.Value.X * w) * _scaleFactor, (p.Y - ScaleCenter.Value.Y * w) * _scaleFactor);
            }
            else
            {
                return new Point(p.X * _scaleFactor, p.Y * _scaleFactor);
            }
        }

        /// <summary>
        /// Centers and Scales ZoomImage to initial position
        /// Also does this based on what aspect the browswer is at.
        /// Behavior between IE and FF is slightly diff since they have slightly different aspects due
        /// to chrome differences.
        /// </summary>
        public void GoHome()
        {
            // hide all markers
            popMarker.Visibility = Visibility.Collapsed;
            characterMarker.Visibility = Visibility.Collapsed;
            processMarker.Visibility = Visibility.Collapsed;
            bShowProcessMarker = false;

            double windowAspect = this.ActualWidth / this.ActualHeight;
            if (windowAspect > 1.5)
            {
                this.ZoomImage.ViewportWidth = 1.7; // 1.45; //a bigger ViewportWidth means smaller or more zoomed out
                this.ZoomImage.ViewportOrigin = new Point(-.35, -.075); //new Point(-.2, -.02);
            }
            else
            {
                this.ZoomImage.ViewportWidth = 1.2;
                this.ZoomImage.ViewportOrigin = new Point(-.1, -.05);
            }
            _zoom = 1;
        }

        /// <summary>
        /// zoom around a point taking into account the zoom upper and bottom limits
        /// </summary>
        /// <param name="newzoom"></param>
        /// <param name="p"></param>
        private void Zoom(double newzoom, Point p)
        {
            minzoom = (double)this.ZoomImage.ActualWidth / (double)width / 2;
            if (newzoom < (minzoom / 2)) { newzoom = (minzoom / 2); }
            if (newzoom > 2) { newzoom = 2; }
            this.ZoomImage.ZoomAboutLogicalPoint(_zoom / newzoom, p.X, p.Y);

            _zoom = newzoom;
        }

        /// <summary>
        /// convert the MSI coordinate space to a rect space
        /// </summary>
        /// <param name="img"></param>
        /// <returns></returns>
        private Rect GetImageRect(MultiScaleImage img)
        {
            return new Rect(-img.ViewportOrigin.X / img.ViewportWidth, -img.ViewportOrigin.Y / img.ViewportWidth, 1 / img.ViewportWidth, 1 / img.ViewportWidth * img.AspectRatio);
        }

        /// <summary>
        /// Animation Stuff: The idea is to run a 0 length animation throughout 
        /// the animation of deep zoom that tracks the motion of the map we then 
        /// synchronize as fast as we can the positioning of other elements on screen
        /// </summary>
        public void TrackAnimation()
        {
            if (_pinSyncAnimation == null)
            {
                _pinSyncAnimation = new Storyboard();
                _pinSyncAnimation.Duration = new Duration(new TimeSpan(0, 0, 0));
                _pinSyncAnimation.Completed += new EventHandler(pinSyncAnimation_Completed);
            }
            startTime = DateTime.Now;

            _pinSyncAnimation.Begin();
        }

        /// <summary>
        /// Create a FrameworkElement specific to the datapoint and add it to a panel
        /// </summary>
        /// <param name="panel"></param>
        /// <param name="datapoint"></param>
        private void AddPinToCanvas(Panel panel, DataPoint datapoint)
        {
            Pin pin = panel.Children.E_FindChild(datapoint.Name) as Pin; // try to get child by name. 

            if (pin == null) // don't add a new "Pin" if it already exists
            {
                pin = new Pin()
                {
                    Name = datapoint.Name,
                    ScaleCenter = datapoint.ScaleCenter,
                    Location = datapoint.Location,
                    DataContext = datapoint,
                };

                // since the Pin.xaml file in this project happens to reside in the Objects folder,
                // any resource that it references must go up one directory when making file requests. 
                // Hence the ../ prefix on the path in the XML file.
                //
                if (!string.IsNullOrEmpty(datapoint.ImagePath))
                    pin.image.Source = new BitmapImage(new Uri(datapoint.ImagePath, UriKind.Relative));

                if (datapoint.Width != null)
                    pin.image.Width = datapoint.Width.Value;

                if (datapoint.Height != null)
                    pin.image.Height = datapoint.Height.Value;

                if (datapoint.Index != null)
                    Canvas.SetZIndex(pin, datapoint.Index.Value);

                //if (!string.IsNullOrEmpty(datapoint.Text))
                //    ToolTipService.SetToolTip(pin, datapoint.Index.Value.ToString());

                pin.MouseLeftButtonDown += new MouseButtonEventHandler(pin_MouseLeftButtonDown);
                pin.MouseEnter += new MouseEventHandler(pin_MouseEnter);
                pin.MouseLeave += new MouseEventHandler(pin_MouseLeave);

                if (datapoint.ImagePathMouseOver != null)
                {
                    pin.MouseEnter += delegate(object sender, MouseEventArgs e)
                    {
                        pin.image.Source = new BitmapImage(new Uri(datapoint.ImagePathMouseOver, UriKind.Relative));
                    };
                }

                if (datapoint.ImagePath != null)
                {
                    pin.MouseLeave += delegate(object sender, MouseEventArgs e)
                    {
                        pin.image.Source = new BitmapImage(new Uri(datapoint.ImagePath, UriKind.Relative));
                    };
                }
                panel.Children.Add(pin);
            }

        }

        /// <summary>
        /// Utility to find a Pin based on the Index property of the Pin class
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        private Pin FindPinByIndex(int index)
        {
            foreach (Pin pin in canvasPinnedObjects.Children)
            {
                if ((pin.DataContext as DataPoint).Index == index)
                {
                    return pin;
                }
            }
            return null;
        }

        /// <summary>
        /// Utility to find a Pin based on the Name property of the Pin class
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        private Pin FindPinByName(string name)
        {
            foreach (Pin pin in canvasPinnedObjects.Children)
            {
                if ((pin.DataContext as DataPoint).Name == name)
                {
                    return pin;
                }
            }
            return null;
        }

        /// <summary>
        /// Application event initialization code
        /// </summary>
        private void SubscribeToEvents()
        {
            // inline event handler for Mouse Zoom
            new MouseWheelHelper(this).Moved += delegate(object sender, MouseWheelEventArgs e)
            {
                processMarker.Visibility = Visibility.Collapsed;
                popMarker.Visibility = Visibility.Collapsed;
                characterMarker.Visibility = Visibility.Collapsed;
                bShowProcessMarker = false;

                double newzoom = _zoom;
                if (e.Delta > 0)
                    newzoom /= 1.3;
                else
                    newzoom *= 1.3;
                Point logicalPoint = this.ZoomImage.ElementToLogicalPoint(this.lastMousePos);
                Zoom(newzoom, this.ZoomImage.ElementToLogicalPoint(this.lastMousePos));
                TrackAnimation();
            };

            this.SizeChanged += new SizeChangedEventHandler(AttachableMultiScaleImage_SizeChanged);
            processMarker.ButtonClicked += new EventHandler<MarkerEventArgs>(ProcessMarker_ButtonClicked);
        }

        #endregion Methods

        #region DependencyProperty

        /* 
         * Though this is not used in this project the Source
         * dependency property enables setting a source path 
         * in the AttachableMultiScaleImage Source attribute in a XAML
         */

        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(string), typeof(AttachableMultiScaleImage),
            new PropertyMetadata((d, e) => ((AttachableMultiScaleImage)d).OnSourceChanged((string)e.OldValue, (string)e.NewValue)));

        public string Source
        {
            get { return (string)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        private void OnSourceChanged(string oldValue, string newValue)
        {
            ZoomImage.Source = new DeepZoomImageTileSource(new Uri(newValue, UriKind.Relative));
        }

        #endregion DependencyProperty

        #region Privates

        bool _isDragging = false;
        bool bShowProcessMarker = false;
        bool ranInitialTrackAnimationMethod = false;
        bool bStartTour = false;

        double _zoom = 1;
        double minzoom;
        double _imageScaleFactor = 1;
        double _scaleFactor = 0;

        int width = 100000000;

        Random r = new Random();
        List<DataPoint> dataPoints = new List<DataPoint>();
        List<Storyboard> storyboards = new List<Storyboard>();
        Pin _processMarkerPositioningPin = new Pin();
        DateTime startTime;
        Storyboard _pinSyncAnimation;
        TimeSpan animationDuration = new TimeSpan(0, 0, 0, 0, 1500); // make the animation last 1500 ms, which is the length of the animation in Deep Zoom

        Point lastMouseDownPos = new Point();
        Point lastMousePos = new Point();
        Point lastMouseViewPort = new Point();

        #endregion Privates

    }
}
